import { beforeEach, describe, expect, it, vi } from 'vitest'

import { LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
import type { IComboWidget } from '@/lib/litegraph/src/types/widgets'
import { ComboWidget } from '@/lib/litegraph/src/widgets/ComboWidget'

const { LGraphCanvas } = await vi.importActual<
  typeof import('@/lib/litegraph/src/LGraphCanvas')
>('@/lib/litegraph/src/LGraphCanvas')
type LGraphCanvasType = InstanceType<typeof LGraphCanvas>

type ContextMenuInstance = {
  addItem?: (
    name: string,
    value: string,
    options: { callback?: (value: string) => void; className?: string }
  ) => void
}

interface MockWidgetConfig extends Omit<IComboWidget, 'options'> {
  options: IComboWidget['options']
}

function createMockWidgetConfig(
  overrides: Partial<MockWidgetConfig> = {}
): MockWidgetConfig {
  return {
    type: 'combo',
    name: 'test',
    value: '',
    options: { values: [] },
    y: 0,
    ...overrides
  }
}

function setupIncrementDecrementTest() {
  const mockCanvas = {
    ds: { scale: 1 },
    last_mouseclick: 1
  } as LGraphCanvasType
  const mockEvent = {} as CanvasPointerEvent
  return { mockCanvas, mockEvent }
}

describe('ComboWidget', () => {
  let node: LGraphNode
  let widget: ComboWidget

  beforeEach(() => {
    vi.clearAllMocks()
    node = new LGraphNode('TestNode')
  })

  describe('_displayValue', () => {
    it('should return value as-is for array values', () => {
      widget = new ComboWidget(
        createMockWidgetConfig({
          name: 'mode',
          value: 'fast',
          options: { values: ['fast', 'slow', 'medium'] }
        }),
        node
      )

      expect(widget._displayValue).toBe('fast')
    })

    it('should return mapped value for object values', () => {
      widget = new ComboWidget(
        createMockWidgetConfig({
          name: 'quality',
          value: 'hq',
          options: {
            values: {
              hq: 'High Quality',
              mq: 'Medium Quality',
              lq: 'Low Quality'
            }
          }
        }),
        node
      )

      expect(widget._displayValue).toBe('High Quality')
    })

    it('should return empty string when disabled', () => {
      widget = new ComboWidget(
        createMockWidgetConfig({
          name: 'mode',
          value: 'fast',
          options: { values: ['fast', 'slow'] },
          computedDisabled: true
        }),
        node
      )

      expect(widget._displayValue).toBe('')
    })

    it('should convert number values to string before display', () => {
      widget = new ComboWidget(
        createMockWidgetConfig({
          name: 'index',
          value: 42,
          options: { values: ['0', '1', '42'] }
        }),
        node
      )

      expect(widget._displayValue).toBe('42')
    })
  })

  describe('canIncrement / canDecrement', () => {
    it('should return true when not at end/start of list', () => {
      widget = new ComboWidget(
        createMockWidgetConfig({
          name: 'mode',
          value: 'medium',
          options: { values: ['fast', 'medium', 'slow'] }
        }),
        node
      )

      expect(widget.canIncrement()).toBe(true)
      expect(widget.canDecrement()).toBe(true)
    })

    it('should return false from canDecrement when at first value', () => {
      widget = new ComboWidget(
        createMockWidgetConfig({
          name: 'mode',
          value: 'fast',
          options: { values: ['fast', 'medium', 'slow'] }
        }),
        node
      )

      expect(widget.canDecrement()).toBe(false)
      expect(widget.canIncrement()).toBe(true)
    })

    it('should return false from canIncrement when at last value', () => {
      widget = new ComboWidget(
        createMockWidgetConfig({
          name: 'mode',
          value: 'slow',
          options: { values: ['fast', 'medium', 'slow'] }
        }),
        node
      )

      expect(widget.canIncrement()).toBe(false)
      expect(widget.canDecrement()).toBe(true)
    })

    it('should return false when list has only one item', () => {
      widget = new ComboWidget(
        createMockWidgetConfig({
          name: 'mode',
          value: 'only',
          options: { values: ['only'] }
        }),
        node
      )

      expect(widget.canIncrement()).toBe(false)
      expect(widget.canDecrement()).toBe(false)
    })

    it('should allow increment/decrement when duplicate values exist at different indices', () => {
      widget = new ComboWidget(
        createMockWidgetConfig({
          name: 'mode',
          value: 'duplicate',
          options: { values: ['duplicate', 'other', 'duplicate'] }
        }),
        node
      )

      expect(widget.canIncrement()).toBe(true)
      expect(widget.canDecrement()).toBe(true)
    })

    it('should return false for function values (DEPRECATED - legacy duck-typed behavior)', () => {
      const valuesFn = vi.fn().mockReturnValue(['a', 'b', 'c'])
      widget = new ComboWidget(
        createMockWidgetConfig({
          name: 'mode',
          value: 'b',
          options: { values: valuesFn }
        }),
        node
      )

      // Function values are legacy - should be permissive (return false)
      expect(widget.canIncrement()).toBe(false)
      expect(widget.canDecrement()).toBe(false)
    })
  })

  describe('incrementValue / decrementValue', () => {
    it('should increment value to next in list', () => {
      widget = new ComboWidget(
        createMockWidgetConfig({
          name: 'mode',
          value: 'fast',
          options: { values: ['fast', 'medium', 'slow'] }
        }),
        node
      )

      const { mockCanvas, mockEvent } = setupIncrementDecrementTest()

      const setValueSpy = vi.spyOn(widget, 'setValue')
      widget.incrementValue({ e: mockEvent, node, canvas: mockCanvas })

      expect(setValueSpy).toHaveBeenCalledWith('medium', {
        e: mockEvent,
        node,
        canvas: mockCanvas
      })
      expect(mockCanvas.last_mouseclick).toBe(0) // Avoid double click event
    })

    it('should decrement value to previous in list', () => {
      widget = new ComboWidget(
        createMockWidgetConfig({
          name: 'mode',
          value: 'medium',
          options: { values: ['fast', 'medium', 'slow'] }
        }),
        node
      )

      const { mockCanvas, mockEvent } = setupIncrementDecrementTest()

      const setValueSpy = vi.spyOn(widget, 'setValue')
      widget.decrementValue({ e: mockEvent, node, canvas: mockCanvas })

      expect(setValueSpy).toHaveBeenCalledWith('fast', {
        e: mockEvent,
        node,
        canvas: mockCanvas
      })
      expect(mockCanvas.last_mouseclick).toBe(0)
    })

    it('should clamp at last value when incrementing beyond', () => {
      widget = new ComboWidget(
        createMockWidgetConfig({
          name: 'mode',
          value: 'slow',
          options: { values: ['fast', 'medium', 'slow'] }
        }),
        node
      )

      const { mockCanvas, mockEvent } = setupIncrementDecrementTest()

      const setValueSpy = vi.spyOn(widget, 'setValue')
      widget.incrementValue({ e: mockEvent, node, canvas: mockCanvas })

      // Should stay at 'slow' (last value)
      expect(setValueSpy).toHaveBeenCalledWith('slow', {
        e: mockEvent,
        node,
        canvas: mockCanvas
      })
    })

    it('should clamp at first value when decrementing beyond', () => {
      widget = new ComboWidget(
        createMockWidgetConfig({
          name: 'mode',
          value: 'fast',
          options: { values: ['fast', 'medium', 'slow'] }
        }),
        node
      )

      const { mockCanvas, mockEvent } = setupIncrementDecrementTest()

      const setValueSpy = vi.spyOn(widget, 'setValue')
      widget.decrementValue({ e: mockEvent, node, canvas: mockCanvas })

      // Should stay at 'fast' (first value)
      expect(setValueSpy).toHaveBeenCalledWith('fast', {
        e: mockEvent,
        node,
        canvas: mockCanvas
      })
    })

    it('should set value to index position when incrementing object-type values', () => {
      widget = new ComboWidget(
        createMockWidgetConfig({
          name: 'quality',
          value: 'hq',
          options: {
            values: {
              hq: 'High Quality',
              mq: 'Medium Quality'
            }
          }
        }),
        node
      )

      const { mockCanvas, mockEvent } = setupIncrementDecrementTest()

      const setValueSpy = vi.spyOn(widget, 'setValue')
      widget.incrementValue({ e: mockEvent, node, canvas: mockCanvas })

      // For object values, setValue receives the index
      expect(setValueSpy).toHaveBeenCalledWith(1, {
        e: mockEvent,
        node,
        canvas: mockCanvas
      })
    })
  })

  describe('onClick', () => {
    it('should decrement value when left arrow clicked', () => {
      widget = new ComboWidget(
        createMockWidgetConfig({
          name: 'mode',
          value: 'medium',
          options: { values: ['fast', 'medium', 'slow'] }
        }),
        node
      )

      const mockCanvas = {
        ds: { scale: 1 },
        last_mouseclick: 0
      } as LGraphCanvasType
      const mockEvent = { canvasX: 60 } as CanvasPointerEvent // 60 - 50 = 10 < 40 (left arrow)
      node.pos = [50, 50]

      const decrementSpy = vi.spyOn(widget, 'decrementValue')
      widget.onClick({ e: mockEvent, node, canvas: mockCanvas })

      expect(decrementSpy).toHaveBeenCalledWith({
        e: mockEvent,
        node,
        canvas: mockCanvas
      })
    })

    it('should increment value when right arrow clicked', () => {
      widget = new ComboWidget(
        createMockWidgetConfig({
          name: 'mode',
          value: 'medium',
          options: { values: ['fast', 'medium', 'slow'] }
        }),
        node
      )

      const mockCanvas = {
        ds: { scale: 1 },
        last_mouseclick: 0
      } as LGraphCanvasType
      const mockEvent = { canvasX: 240 } as CanvasPointerEvent
      node.pos = [50, 50]
      node.size = [200, 30]

      const incrementSpy = vi.spyOn(widget, 'incrementValue')
      widget.onClick({ e: mockEvent, node, canvas: mockCanvas })

      expect(incrementSpy).toHaveBeenCalledWith({
        e: mockEvent,
        node,
        canvas: mockCanvas
      })
    })

    it('should show dropdown menu when clicking center area with array values', () => {
      widget = new ComboWidget(
        createMockWidgetConfig({
          name: 'mode',
          value: 'medium',
          options: { values: ['fast', 'medium', 'slow'] }
        }),
        node
      )

      const mockCanvas = { ds: { scale: 1 } } as LGraphCanvasType
      const mockEvent = { canvasX: 150 } as CanvasPointerEvent
      node.pos = [50, 50]
      node.size = [200, 30]

      const mockContextMenu = vi.fn()
      LiteGraph.ContextMenu =
        mockContextMenu as unknown as typeof LiteGraph.ContextMenu

      widget.onClick({ e: mockEvent, node, canvas: mockCanvas })

      expect(mockContextMenu).toHaveBeenCalledWith(
        ['fast', 'medium', 'slow'],
        expect.objectContaining({
          scale: 1,
          event: mockEvent,
          className: 'dark'
        })
      )
    })

    it('should show dropdown menu with object display values', () => {
      widget = new ComboWidget(
        createMockWidgetConfig({
          name: 'quality',
          value: 'mq',
          options: {
            values: {
              hq: 'High Quality',
              mq: 'Medium Quality',
              lq: 'Low Quality'
            }
          }
        }),
        node
      )

      const mockCanvas = { ds: { scale: 1 } } as LGraphCanvasType
      const mockEvent = { canvasX: 150 } as CanvasPointerEvent
      node.pos = [50, 50]
      node.size = [200, 30]

      const mockContextMenu = vi.fn()
      LiteGraph.ContextMenu =
        mockContextMenu as unknown as typeof LiteGraph.ContextMenu

      widget.onClick({ e: mockEvent, node, canvas: mockCanvas })

      // Should show the display values (values), not keys
      expect(mockContextMenu).toHaveBeenCalledWith(
        ['High Quality', 'Medium Quality', 'Low Quality'],
        expect.objectContaining({
          scale: 1,
          event: mockEvent,
          className: 'dark'
        })
      )
    })

    it('should set value when selecting from dropdown with array values', () => {
      widget = new ComboWidget(
        createMockWidgetConfig({
          name: 'mode',
          value: 'fast',
          options: { values: ['fast', 'medium', 'slow'] }
        }),
        node
      )

      const mockCanvas = { ds: { scale: 1 } } as LGraphCanvasType
      const mockEvent = { canvasX: 150 } as CanvasPointerEvent
      node.pos = [50, 50]
      node.size = [200, 30]

      let capturedCallback: ((value: string) => void) | undefined
      const mockContextMenu = vi.fn((_values, options) => {
        capturedCallback = options.callback
        return {} as ContextMenuInstance
      })
      LiteGraph.ContextMenu =
        mockContextMenu as unknown as typeof LiteGraph.ContextMenu

      const setValueSpy = vi.spyOn(widget, 'setValue')
      widget.onClick({ e: mockEvent, node, canvas: mockCanvas })

      // Simulate selecting 'slow' from dropdown
      capturedCallback?.('slow')

      expect(setValueSpy).toHaveBeenCalledWith('slow', {
        e: mockEvent,
        node,
        canvas: mockCanvas
      })
    })

    it('should set value to selected index for object-type values', () => {
      widget = new ComboWidget(
        createMockWidgetConfig({
          name: 'quality',
          value: 'hq',
          options: {
            values: {
              hq: 'High Quality',
              mq: 'Medium Quality',
              lq: 'Low Quality'
            }
          }
        }),
        node
      )

      const mockCanvas = { ds: { scale: 1 } } as LGraphCanvasType
      const mockEvent = { canvasX: 150 } as CanvasPointerEvent
      node.pos = [50, 50]
      node.size = [200, 30]

      let capturedCallback: ((value: string) => void) | undefined
      const mockContextMenu = vi.fn((_values, options) => {
        capturedCallback = options.callback
        return {} as ContextMenuInstance
      })
      LiteGraph.ContextMenu =
        mockContextMenu as unknown as typeof LiteGraph.ContextMenu

      const setValueSpy = vi.spyOn(widget, 'setValue')
      widget.onClick({ e: mockEvent, node, canvas: mockCanvas })

      // Simulate selecting 'Medium Quality' (index 1) from dropdown
      capturedCallback?.('Medium Quality')

      expect(setValueSpy).toHaveBeenCalledWith(1, {
        e: mockEvent,
        node,
        canvas: mockCanvas
      })
    })

    it('should prevent menu scaling below 100%', () => {
      widget = new ComboWidget(
        createMockWidgetConfig({
          name: 'mode',
          value: 'fast',
          options: { values: ['fast', 'slow'] }
        }),
        node
      )

      const mockCanvas = { ds: { scale: 0.5 } } as LGraphCanvasType
      const mockEvent = { canvasX: 150 } as CanvasPointerEvent
      node.pos = [50, 50]
      node.size = [200, 30]

      const mockContextMenu = vi.fn()
      LiteGraph.ContextMenu =
        mockContextMenu as unknown as typeof LiteGraph.ContextMenu

      widget.onClick({ e: mockEvent, node, canvas: mockCanvas })

      expect(mockContextMenu).toHaveBeenCalledWith(
        expect.any(Array),
        expect.objectContaining({
          scale: 1 // Math.max(1, 0.5) = 1
        })
      )
    })

    it('should warn when using deprecated function values', () => {
      const deprecationCallback = vi.fn()
      const originalCallbacks = LiteGraph.onDeprecationWarning
      LiteGraph.onDeprecationWarning = [deprecationCallback]

      const valuesFn = vi.fn().mockReturnValue(['a', 'b', 'c'])
      widget = new ComboWidget(
        createMockWidgetConfig({
          name: 'mode',
          value: 'a',
          options: { values: valuesFn }
        }),
        node
      )

      const mockCanvas = { ds: { scale: 1 } } as LGraphCanvasType
      const mockEvent = { canvasX: 150 } as CanvasPointerEvent
      node.pos = [50, 50]
      node.size = [200, 30]

      const mockContextMenu = vi.fn()
      LiteGraph.ContextMenu =
        mockContextMenu as unknown as typeof LiteGraph.ContextMenu

      widget.onClick({ e: mockEvent, node, canvas: mockCanvas })

      expect(deprecationCallback).toHaveBeenCalledWith(
        'Using a function for values is deprecated. Use an array of unique values instead.',
        undefined
      )

      LiteGraph.onDeprecationWarning = originalCallbacks
    })
  })

  describe('with getOptionLabel', () => {
    const HASH_FILENAME =
      '72e786ff2a44d682c4294db0b7098e569832bc394efc6dad644e6ec85a78efb7.png'
    const HASH_FILENAME_2 =
      'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456.jpg'

    describe('_displayValue', () => {
      it('should return formatted value when getOptionLabel provided', () => {
        const mockGetOptionLabel = vi
          .fn()
          .mockReturnValue('Beautiful Sunset.png')

        widget = new ComboWidget(
          createMockWidgetConfig({
            name: 'image',
            value: HASH_FILENAME,
            options: {
              values: [HASH_FILENAME],
              getOptionLabel: mockGetOptionLabel
            }
          }),
          node
        )

        expect(widget._displayValue).toBe('Beautiful Sunset.png')
        expect(mockGetOptionLabel).toHaveBeenCalledWith(HASH_FILENAME)
      })

      it('should return original value when getOptionLabel not provided', () => {
        widget = new ComboWidget(
          createMockWidgetConfig({
            name: 'image',
            value: HASH_FILENAME,
            options: { values: [HASH_FILENAME] }
          }),
          node
        )

        expect(widget._displayValue).toBe(HASH_FILENAME)
      })

      it('should not call getOptionLabel when disabled', () => {
        const mockGetOptionLabel = vi.fn()

        widget = new ComboWidget(
          createMockWidgetConfig({
            name: 'image',
            value: HASH_FILENAME,
            options: {
              values: [HASH_FILENAME],
              getOptionLabel: mockGetOptionLabel
            },
            computedDisabled: true
          }),
          node
        )

        expect(widget._displayValue).toBe('')
        expect(mockGetOptionLabel).not.toHaveBeenCalled()
      })

      it('should handle getOptionLabel error gracefully', () => {
        const mockGetOptionLabel = vi.fn().mockImplementation(() => {
          throw new Error('Formatting failed')
        })
        const consoleErrorSpy = vi
          .spyOn(console, 'error')
          .mockImplementation(() => {})

        widget = new ComboWidget(
          createMockWidgetConfig({
            name: 'image',
            value: HASH_FILENAME,
            options: {
              values: [HASH_FILENAME],
              getOptionLabel: mockGetOptionLabel
            }
          }),
          node
        )

        expect(widget._displayValue).toBe(HASH_FILENAME)
        expect(consoleErrorSpy).toHaveBeenCalledWith(
          'Failed to map value:',
          expect.any(Error)
        )

        consoleErrorSpy.mockRestore()
      })

      it('should format non-hash filenames using getOptionLabel', () => {
        const mockGetOptionLabel = vi.fn((value) => `Formatted ${value}`)

        widget = new ComboWidget(
          createMockWidgetConfig({
            name: 'file',
            value: 'regular-file.png',
            options: {
              values: ['regular-file.png'],
              getOptionLabel: mockGetOptionLabel
            }
          }),
          node
        )

        expect(widget._displayValue).toBe('Formatted regular-file.png')
        expect(mockGetOptionLabel).toHaveBeenCalledWith('regular-file.png')
      })

      it('should use getOptionLabel over object value mapping when both present', () => {
        const mockGetOptionLabel = vi.fn((value) => `Label: ${value}`)

        widget = new ComboWidget(
          createMockWidgetConfig({
            name: 'quality',
            value: 'hq',
            options: {
              values: {
                hq: 'High Quality',
                mq: 'Medium Quality'
              },
              getOptionLabel: mockGetOptionLabel
            }
          }),
          node
        )

        // getOptionLabel should take precedence over object value mapping
        expect(widget._displayValue).toBe('Label: hq')
        expect(mockGetOptionLabel).toHaveBeenCalledWith('hq')
      })

      it('should format number values using getOptionLabel when provided', () => {
        const mockGetOptionLabel = vi.fn((value) => `Number: ${value}`)

        widget = new ComboWidget(
          createMockWidgetConfig({
            name: 'index',
            value: 42,
            options: {
              values: ['0', '1', '42'],
              getOptionLabel: mockGetOptionLabel
            }
          }),
          node
        )

        expect(widget._displayValue).toBe('Number: 42')
        expect(mockGetOptionLabel).toHaveBeenCalledWith('42')
      })
    })

    describe('onClick', () => {
      it('should show dropdown with formatted labels', () => {
        const mockGetOptionLabel = vi
          .fn()
          .mockReturnValueOnce('Beautiful Sunset.png')
          .mockReturnValueOnce('Mountain Vista.jpg')

        widget = new ComboWidget(
          createMockWidgetConfig({
            name: 'image',
            value: HASH_FILENAME,
            options: {
              values: [HASH_FILENAME, HASH_FILENAME_2],
              getOptionLabel: mockGetOptionLabel
            }
          }),
          node
        )

        const mockCanvas = { ds: { scale: 1 } } as LGraphCanvasType
        const mockEvent = { canvasX: 150 } as CanvasPointerEvent
        node.pos = [50, 50]
        node.size = [200, 30]

        const mockAddItem = vi.fn()
        const mockContextMenu = vi.fn(() => ({ addItem: mockAddItem }))
        LiteGraph.ContextMenu =
          mockContextMenu as unknown as typeof LiteGraph.ContextMenu

        widget.onClick({ e: mockEvent, node, canvas: mockCanvas })

        // Should show formatted labels in dropdown
        expect(mockContextMenu).toHaveBeenCalledWith(
          [],
          expect.objectContaining({
            scale: 1,
            event: mockEvent,
            className: 'dark'
          })
        )

        expect(mockAddItem).toHaveBeenCalledWith(
          'Beautiful Sunset.png',
          HASH_FILENAME,
          expect.objectContaining({
            callback: expect.any(Function),
            className: 'dark'
          })
        )
        expect(mockAddItem).toHaveBeenCalledWith(
          'Mountain Vista.jpg',
          HASH_FILENAME_2,
          expect.objectContaining({
            callback: expect.any(Function),
            className: 'dark'
          })
        )
      })

      it('should set original value when selecting formatted label from dropdown', () => {
        const mockGetOptionLabel = vi
          .fn()
          .mockReturnValueOnce('Beautiful Sunset.png')
          .mockReturnValueOnce('Mountain Vista.jpg')

        widget = new ComboWidget(
          createMockWidgetConfig({
            name: 'image',
            value: HASH_FILENAME,
            options: {
              values: [HASH_FILENAME, HASH_FILENAME_2],
              getOptionLabel: mockGetOptionLabel
            }
          }),
          node
        )

        const mockCanvas = { ds: { scale: 1 } } as LGraphCanvasType
        const mockEvent = { canvasX: 150 } as CanvasPointerEvent
        node.pos = [50, 50]
        node.size = [200, 30]

        const mockAddItem = vi.fn()
        let capturedCallback: ((value: string) => void) | undefined
        const mockContextMenu = vi.fn((_values, options) => {
          capturedCallback = options.callback
          return { addItem: mockAddItem }
        })
        LiteGraph.ContextMenu =
          mockContextMenu as unknown as typeof LiteGraph.ContextMenu

        const setValueSpy = vi.spyOn(widget, 'setValue')
        widget.onClick({ e: mockEvent, node, canvas: mockCanvas })

        // Simulate selecting second item (Mountain Vista.jpg -> HASH_FILENAME_2)
        capturedCallback?.(HASH_FILENAME_2)

        // Should set the actual hash value, not the formatted label
        expect(setValueSpy).toHaveBeenCalledWith(HASH_FILENAME_2, {
          e: mockEvent,
          node,
          canvas: mockCanvas
        })
      })

      it('should preserve value identity when multiple options have same display label', () => {
        const mockGetOptionLabel = vi
          .fn()
          .mockReturnValueOnce('sunset.png')
          .mockReturnValueOnce('sunset.png') // Same label, different values
          .mockReturnValueOnce('mountain.png')

        const hash1 = HASH_FILENAME
        const hash2 = HASH_FILENAME_2
        const hash3 = 'abc123def456.png'

        widget = new ComboWidget(
          createMockWidgetConfig({
            name: 'image',
            value: hash1,
            options: {
              values: [hash1, hash2, hash3],
              getOptionLabel: mockGetOptionLabel
            }
          }),
          node
        )

        const mockCanvas = { ds: { scale: 1 } } as LGraphCanvasType
        const mockEvent = { canvasX: 150 } as CanvasPointerEvent
        node.pos = [50, 50]
        node.size = [200, 30]

        const mockAddItem = vi.fn()
        let capturedCallback: ((value: string) => void) | undefined

        const mockContextMenu = vi.fn((_values, options) => {
          capturedCallback = options.callback
          return { addItem: mockAddItem } as ContextMenuInstance
        })
        LiteGraph.ContextMenu =
          mockContextMenu as unknown as typeof LiteGraph.ContextMenu

        widget.onClick({ e: mockEvent, node, canvas: mockCanvas })

        // Should use addItem API with separate name/value
        expect(mockAddItem).toHaveBeenCalledWith(
          'sunset.png',
          hash1,
          expect.objectContaining({
            callback: expect.any(Function),
            className: 'dark'
          })
        )
        expect(mockAddItem).toHaveBeenCalledWith(
          'sunset.png',
          hash2,
          expect.objectContaining({
            callback: expect.any(Function),
            className: 'dark'
          })
        )
        expect(mockAddItem).toHaveBeenCalledWith(
          'mountain.png',
          hash3,
          expect.objectContaining({
            callback: expect.any(Function),
            className: 'dark'
          })
        )

        const setValueSpy = vi.spyOn(widget, 'setValue')

        // Simulate selecting the SECOND "sunset.png" (should pass hash2 directly)
        capturedCallback?.(hash2)

        // Should set hash2, not hash1 (fixes duplicate name bug)
        expect(setValueSpy).toHaveBeenCalledWith(hash2, {
          e: mockEvent,
          node,
          canvas: mockCanvas
        })
      })

      it('should handle getOptionLabel error in dropdown gracefully', () => {
        const mockGetOptionLabel = vi
          .fn()
          .mockReturnValueOnce('Beautiful Sunset.png')
          .mockImplementationOnce(() => {
            throw new Error('Formatting failed')
          })

        widget = new ComboWidget(
          createMockWidgetConfig({
            name: 'image',
            value: HASH_FILENAME,
            options: {
              values: [HASH_FILENAME, HASH_FILENAME_2],
              getOptionLabel: mockGetOptionLabel
            }
          }),
          node
        )

        const mockCanvas = { ds: { scale: 1 } } as LGraphCanvasType
        const mockEvent = { canvasX: 150 } as CanvasPointerEvent
        node.pos = [50, 50]
        node.size = [200, 30]

        const consoleErrorSpy = vi
          .spyOn(console, 'error')
          .mockImplementation(() => {})

        const mockAddItem = vi.fn()
        const mockContextMenu = vi.fn(() => {
          return { addItem: mockAddItem } as ContextMenuInstance
        })
        LiteGraph.ContextMenu =
          mockContextMenu as unknown as typeof LiteGraph.ContextMenu

        widget.onClick({ e: mockEvent, node, canvas: mockCanvas })

        // Should show formatted label for first, fallback to hash for second
        expect(mockAddItem).toHaveBeenCalledWith(
          'Beautiful Sunset.png',
          HASH_FILENAME,
          expect.objectContaining({
            callback: expect.any(Function),
            className: 'dark'
          })
        )
        expect(mockAddItem).toHaveBeenCalledWith(
          HASH_FILENAME_2,
          HASH_FILENAME_2,
          expect.objectContaining({
            callback: expect.any(Function),
            className: 'dark'
          })
        )
        expect(consoleErrorSpy).toHaveBeenCalledWith(
          'Failed to map value:',
          expect.any(Error)
        )

        consoleErrorSpy.mockRestore()
      })

      it('should show hash values in dropdown when getOptionLabel not provided', () => {
        widget = new ComboWidget(
          createMockWidgetConfig({
            name: 'image',
            value: HASH_FILENAME,
            options: {
              values: [HASH_FILENAME, HASH_FILENAME_2]
            }
          }),
          node
        )

        const mockCanvas = { ds: { scale: 1 } } as LGraphCanvasType
        const mockEvent = { canvasX: 150 } as CanvasPointerEvent
        node.pos = [50, 50]
        node.size = [200, 30]

        const mockContextMenu = vi.fn()
        LiteGraph.ContextMenu =
          mockContextMenu as unknown as typeof LiteGraph.ContextMenu

        widget.onClick({ e: mockEvent, node, canvas: mockCanvas })

        // Should show hash filenames directly (no formatting)
        expect(mockContextMenu).toHaveBeenCalledWith(
          [HASH_FILENAME, HASH_FILENAME_2],
          expect.objectContaining({
            scale: 1,
            event: mockEvent,
            className: 'dark'
          })
        )
      })
    })
  })

  describe('edge cases', () => {
    it('should return empty display value and disallow increment/decrement for empty values', () => {
      widget = new ComboWidget(
        createMockWidgetConfig({
          name: 'mode',
          value: '',
          options: { values: [] }
        }),
        node
      )

      expect(widget._displayValue).toBe('')
      expect(widget.canIncrement()).toBe(false)
      expect(widget.canDecrement()).toBe(false)
    })

    it('should throw error when values is null in getValues', () => {
      widget = new ComboWidget(
        createMockWidgetConfig({
          name: 'mode',
          value: 'test',
          options: { values: null as any }
        }),
        node
      )

      const mockCanvas = { ds: { scale: 1 } } as LGraphCanvasType
      const mockEvent = { canvasX: 150 } as CanvasPointerEvent
      node.pos = [50, 50]
      node.size = [200, 30]

      expect(() => {
        widget.onClick({ e: mockEvent, node, canvas: mockCanvas })
      }).toThrow('[ComboWidget]: values is required')
    })

    it('should default to first value when incrementing from invalid value', () => {
      widget = new ComboWidget(
        createMockWidgetConfig({
          name: 'mode',
          value: 'nonexistent',
          options: { values: ['fast', 'medium', 'slow'] }
        }),
        node
      )

      const { mockCanvas, mockEvent } = setupIncrementDecrementTest()

      const setValueSpy = vi.spyOn(widget, 'setValue')
      widget.incrementValue({ e: mockEvent, node, canvas: mockCanvas })

      // When value not found (indexOf returns -1), -1 + 1 = 0, clamped to 0
      expect(setValueSpy).toHaveBeenCalledWith('fast', {
        e: mockEvent,
        node,
        canvas: mockCanvas
      })
    })
  })
})
